/*
 * Copyright (C) 2021 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.datatheorem.ohos.trustkit.config;

import ohos.app.Context;
import ohos.global.resource.NotExistException;
import ohos.global.resource.WrongTypeException;


import com.datatheorem.ohos.trustkit.ResourceTable;

import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Arrays;
import java.util.Date;
import java.util.HashSet;
import java.util.Set;

public final class DomainPinningPolicy {

    // The default URL to submit pin failure report to
    private java.net.URL defaultUrl = null;

    private final String hostname;
    private final boolean shouldIncludeSubdomains;
    private final Set<PublicKeyPin> publicKeyPins;
    public final Date expirationDate;
    private final boolean shouldEnforcePinning;
    private final Set<URL> reportUris;

    DomainPinningPolicy(String default_url,
                        String hostname,
                        Boolean shouldIncludeSubdomains,
                        Set<String> publicKeyHashStrList,
                        Boolean shouldEnforcePinning,
                        Date expirationDate,
                        Set<String> reportUriStrList,
                        Boolean shouldDisableDefaultReportUri)
            throws MalformedURLException {
        // Run some sanity checks on the configuration
        // Check if the hostname seems valid
        DomainValidator domainValidator = DomainValidator.getInstance();
        if (!domainValidator.isValid(hostname)) {
            throw new ConfigurationException("Tried to pin an invalid domain: " + hostname);
        }
        this.hostname = hostname.trim();

        // Due to the fact some configurations could be added without any pin (e.g. localhost)
        // the publicKeyHashStrList would be null.
        // Thus we're managing these cases as an empty set of pins.
        if (publicKeyHashStrList == null)
            publicKeyHashStrList = new HashSet<>();

        // Parse boolean settings and handle default values
        if (shouldEnforcePinning == null) {
            this.shouldEnforcePinning = false;
        } else {
            this.shouldEnforcePinning = shouldEnforcePinning;
        }
        if (shouldIncludeSubdomains == null) {
            this.shouldIncludeSubdomains = false;
        } else {
            this.shouldIncludeSubdomains = shouldIncludeSubdomains;
        }


        // Check if the configuration has a empty pin-set and still would enforce pinning
        // TrustKit should not work if the configuration contains both (opposite behaviors)
        if (publicKeyHashStrList.isEmpty() && this.shouldEnforcePinning) {
            throw new ConfigurationException("An empty pin-set was supplied " +
                    "for domain " + this.hostname + " with the enforcePinning set to true. " +
                    "An empty pin-set disables pinning and can't be use with enforcePinning set to true.");
        }

        // Check if the configuration has at least two pins (including a backup pin)
        // TrustKit should not work if the configuration contains only one pin
        // more info (https://tools.ietf.org/html/rfc7469#page-21)
        if (publicKeyHashStrList.size() < 2 && this.shouldEnforcePinning) {
            throw new ConfigurationException("Less than two pins were supplied " +
                    "for domain " + this.hostname + ". This might " +
                    "brick your App; please review the Getting Started guide in " +
                    "./docs/getting-started.md");
        }

        // Parse the supplied pins
        publicKeyPins = new HashSet<>();
        for (String pinStr : publicKeyHashStrList) {
            publicKeyPins.add(new PublicKeyPin(pinStr));
        }

        // Parse the supplied report URLs
        reportUris = new HashSet<>();
        if (reportUriStrList != null) {
            for (String UriStr : reportUriStrList) {
                reportUris.add(new URL(UriStr));
            }
        }

        // Add the default report URL
        if ((shouldDisableDefaultReportUri == null) || (!shouldDisableDefaultReportUri)) {

            try {
                defaultUrl = new java.net.URL(default_url);

            } catch (java.net.MalformedURLException e) {
                throw new IllegalStateException("Bad DEFAULT_REPORTING_URL");
            }
            reportUris.add(defaultUrl);
        }

        this.expirationDate = expirationDate;
    }

    public String getHostname() {
        return hostname;
    }

    public Set<PublicKeyPin> getPublicKeyPins() {
        return publicKeyPins;
    }

    public boolean shouldEnforcePinning() {
        return shouldEnforcePinning;
    }

    public Set<URL> getReportUris() {
        return reportUris;
    }

    public boolean shouldIncludeSubdomains() {
        return shouldIncludeSubdomains;
    }

    @Override
    public String toString() {
        return "DomainPinningPolicy{" +
                "hostname = " + hostname + "\n" +
                "knownPins = " + Arrays.toString(publicKeyPins.toArray()) +
                "\n" +
                "shouldEnforcePinning = " + shouldEnforcePinning + "\n" +
                "reportUris = " + reportUris + "\n" +
                "shouldIncludeSubdomains = " + shouldIncludeSubdomains + "\n" +
                "}";
    }


    public static final class Builder {
        // The domain must always be specified in domain-config
        private String hostname;

        // The remaining settings can be inherited from a parent domain-config
        private Boolean shouldIncludeSubdomains;
        private Set<String> publicKeyHashes;
        private Date expirationDate;
        private Boolean shouldEnforcePinning;
        private Set<String> reportUris;
        private Boolean shouldDisableDefaultReportUri;

        // The parent domain-config
        private Builder parentBuilder = null;

        public DomainPinningPolicy build(Context context) throws MalformedURLException {
            if (parentBuilder != null) {
                // Get missing values from the parent as some entries can be inherited
                // build() should already have been called on it so it has its parent's values
                // inherited already
                if (shouldIncludeSubdomains == null) {
                    shouldIncludeSubdomains = parentBuilder.getShouldIncludeSubdomains();
                }

                if (publicKeyHashes == null) {
                    publicKeyHashes = parentBuilder.getPublicKeyHashes();
                }

                if (expirationDate == null) {
                    expirationDate = parentBuilder.getExpirationDate();
                }

                if (shouldEnforcePinning == null) {
                    shouldEnforcePinning = parentBuilder.getShouldEnforcePinning();
                }

                if (reportUris == null) {
                    reportUris = parentBuilder.getReportUris();
                }

                if (shouldDisableDefaultReportUri == null) {
                    shouldDisableDefaultReportUri = parentBuilder.getShouldDisableDefaultReportUri();
                }
            }

            if (publicKeyHashes == null) {
                // This configuration entry is not for pinning but something else; skip it
                return null;
            }
            DomainPinningPolicy mDomainPinningPolicy = null;
            try {
                mDomainPinningPolicy = new DomainPinningPolicy(
                        context.getResourceManager().getElement(ResourceTable.String_default_url).getString(),

                        hostname,
                        shouldIncludeSubdomains,
                        publicKeyHashes,
                        shouldEnforcePinning,
                        expirationDate,
                        reportUris,
                        shouldDisableDefaultReportUri
                );
            } catch (IOException e) {
                e.printStackTrace();
            } catch (NotExistException e) {
                e.printStackTrace();
            } catch (WrongTypeException e) {
                e.printStackTrace();
            }
            return mDomainPinningPolicy;
        }

        public Builder setParent(Builder parent) {
            // Sanity check to avoid adding loops.
            Builder current = parent;
            while (current != null) {
                if (current == this) {
                    throw new IllegalArgumentException("Loops are not allowed in Builder parents");
                }
                current = current.parentBuilder;
            }
            parentBuilder = parent;
            return this;
        }

        public Builder setHostname(String hostname) {
            this.hostname = hostname;
            return this;
        }

        Boolean getShouldIncludeSubdomains() {
            return shouldIncludeSubdomains;
        }

        public Builder setShouldIncludeSubdomains(Boolean shouldIncludeSubdomains) {
            this.shouldIncludeSubdomains = shouldIncludeSubdomains;
            return this;
        }

        Set<String> getPublicKeyHashes() {
            return publicKeyHashes;
        }

        public Builder setPublicKeyHashes(Set<String> publicKeyHashes) {
            this.publicKeyHashes = publicKeyHashes;
            return this;
        }

        Date getExpirationDate() {
            return expirationDate;
        }

        Builder setExpirationDate(Date expirationDate) {
            this.expirationDate = expirationDate;
            return this;
        }

        Boolean getShouldEnforcePinning() {
            return shouldEnforcePinning;
        }

        public Builder setShouldEnforcePinning(Boolean shouldEnforcePinning) {
            this.shouldEnforcePinning = shouldEnforcePinning;
            return this;
        }

        Set<String> getReportUris() {
            return reportUris;
        }

        public Builder setReportUris(Set<String> reportUris) {
            this.reportUris = reportUris;
            return this;
        }

        Boolean getShouldDisableDefaultReportUri() {
            return shouldDisableDefaultReportUri;
        }

        public Builder setShouldDisableDefaultReportUri(Boolean shouldDisableDefaultReportUri) {
            this.shouldDisableDefaultReportUri = shouldDisableDefaultReportUri;
            return this;
        }
    }
}

